/*
Copyright 2025 The KServe Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package llmisvc

import (
	"context"
	"fmt"
	"reflect"
	"strings"

	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/tools/record"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
	"sigs.k8s.io/controller-runtime/pkg/log"
)

// clientWithRecorder combines the client and event recorder interfaces
// This allows CRUD operations to both modify resources and emit events
type clientWithRecorder interface {
	client.Client
	record.EventRecorder
}

// Create creates a new Kubernetes resource and emits an event on the owner
// This provides a standardized way to create owned resources with proper event logging
func Create[O client.Object, T client.Object](ctx context.Context, c clientWithRecorder, owner O, expected T) error {
	if err := c.Create(ctx, expected); err != nil {
		return fmt.Errorf("failed to create %s %s/%s: %w", logLineForObject(expected), expected.GetNamespace(), expected.GetName(), err)
	}

	if !reflect.ValueOf(owner).IsNil() {
		c.Eventf(owner, corev1.EventTypeNormal, "Created", "Created %s %s/%s", logLineForObject(expected), expected.GetNamespace(), expected.GetName())
	}

	return nil
}

// Delete safely deletes a Kubernetes resource with ownership validation
// It ensures only the owner can delete the resource and handles garbage collection properly
func Delete[O client.Object, T client.Object](ctx context.Context, c clientWithRecorder, owner O, expected T) error {
	typeLogLine := logLineForObject(expected)
	isOwnerNil := reflect.ValueOf(owner).IsNil()

	ownerLogLine := ""
	if !isOwnerNil {
		ownerLogLine = logLineForObject(owner)
	}

	if isNamespaced, err := apiutil.IsObjectNamespaced(expected, c.Scheme(), c.RESTMapper()); err != nil && !meta.IsNoMatchError(err) {
		return fmt.Errorf("failed to resolve if resource is namespaced %s: %w", typeLogLine, err)
	} else if isNamespaced && !isOwnerNil {
		if !metav1.IsControlledBy(expected, owner) {
			return fmt.Errorf("failed to delete %s %s/%s: it is not controlled by %s %s/%s",
				typeLogLine,
				expected.GetNamespace(), expected.GetName(),
				ownerLogLine,
				owner.GetNamespace(), owner.GetName(),
			)
		} else if !owner.GetDeletionTimestamp().IsZero() {
			// If the owner is being deleted, assume the owned resource is going
			// to be automatically garbage colleted by the cluster
			return nil
		}
	}

	// Don't re-try deletion, if the owned object is already being deleted
	if !expected.GetDeletionTimestamp().IsZero() {
		return nil
	}

	if err := c.Delete(ctx, expected); err != nil {
		if !apierrors.IsNotFound(err) && !meta.IsNoMatchError(err) {
			return fmt.Errorf("failed to delete %s %s/%s: %w", typeLogLine, expected.GetNamespace(), expected.GetName(), err)
		}
		return nil
	}

	if !isOwnerNil {
		c.Eventf(owner, corev1.EventTypeNormal, "Deleted", "Deleted %s %s/%s", typeLogLine, expected.GetNamespace(), expected.GetName())
	}

	return nil
}

// Reconcile ensures a resource exists and matches the expected state
// It creates the resource if it doesn't exist, or updates it if it differs from expected state
func Reconcile[O client.Object, T client.Object](ctx context.Context, c clientWithRecorder, owner O, empty, expected T, isEqual SemanticEqual[T]) error {
	typeLogLine := logLineForObject(expected)

	// Try to fetch the current state of the resource
	curr := empty.DeepCopyObject().(T)
	if err := c.Get(ctx, client.ObjectKeyFromObject(expected), curr); err != nil {
		if client.IgnoreNotFound(err) != nil {
			return fmt.Errorf("failed to get %s %s/%s: %w", typeLogLine, expected.GetNamespace(), expected.GetName(), err)
		}
		// Resource doesn't exist, create it
		return Create(ctx, c, owner, expected)
	}
	// Resource exists, update it if necessary
	return Update(ctx, c, owner, curr, expected, isEqual)
}

// Update modifies an existing Kubernetes resource to match the expected state
// It validates ownership and only updates if the resource has actually changed
func Update[O client.Object, T client.Object](ctx context.Context, c clientWithRecorder, owner O, curr, expected T, isEqual SemanticEqual[T]) error {
	typeLogLine := logLineForObject(expected)
	isOwnerNil := reflect.ValueOf(owner).IsNil()

	ownerLogLine := ""
	if !isOwnerNil {
		ownerLogLine = logLineForObject(owner)
	}

	if isNamespaced, err := apiutil.IsObjectNamespaced(expected, c.Scheme(), c.RESTMapper()); err != nil {
		return fmt.Errorf("failed to resolve if resource is namespaced %s: %w", typeLogLine, err)
	} else if isNamespaced && !isOwnerNil {
		if !metav1.IsControlledBy(curr, owner) {
			return fmt.Errorf("failed to update %s %s/%s: it is not controlled by %s %s/%s",
				typeLogLine,
				curr.GetNamespace(), curr.GetName(),
				ownerLogLine,
				owner.GetNamespace(), owner.GetName(),
			)
		}
	}

	expected.SetResourceVersion(curr.GetResourceVersion())
	if err := c.Update(ctx, expected, client.DryRunAll); err != nil {
		return fmt.Errorf("failed to get defaults for %s %s/%s: %w", typeLogLine, expected.GetNamespace(), expected.GetName(), err)
	}

	if isEqual(expected, curr) {
		return nil
	}

	log.FromContext(ctx).V(2).Info("Updating "+typeLogLine,
		"expected", expected,
		"curr", curr,
	)

	if err := c.Update(ctx, expected); err != nil {
		return fmt.Errorf("failed to update %s %s/%s: %w", typeLogLine, expected.GetNamespace(), expected.GetName(), err)
	}

	if !isOwnerNil {
		c.Eventf(owner, corev1.EventTypeNormal, "Updated", "Updated %s %s/%s", typeLogLine, expected.GetNamespace(), expected.GetName())
	}

	return nil
}

// logLineForObject returns a human-readable type name for logging
// We use reflection since GetObjectKind() is often empty in practice
func logLineForObject(obj client.Object) string {
	// Note: don't use `obj.GetObjectKind()` as it's always empty.
	return strings.Replace(reflect.TypeOf(obj).String(), "*", "", 1)
}

// SemanticEqual is a function type for comparing two objects to determine if they are equivalent
// This allows custom comparison logic beyond simple equality checks
type SemanticEqual[T client.Object] func(expected T, curr T) bool
